{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "iQjHqsmTAVLU"
   },
   "source": [
    "# Week 3: Improve MNIST with Convolutions\n",
    "\n",
    "In the lectures you looked at how you would improve Fashion MNIST using Convolutions. For this assignment, see if you can improve MNIST to 99.5% accuracy or more by adding only a single convolutional layer and a single MaxPooling 2D layer to the model from the  assignment of the previous week. \n",
    "\n",
    "\n",
    "Some notes:\n",
    "1. Your network should succeed in less than 10 epochs.\n",
    "2. When it reaches 99.5% or greater it should print out the string \"Reached 99.5% accuracy so cancelling training!\" and stop training."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### TIPS FOR SUCCESSFUL GRADING OF YOUR ASSIGNMENT:\n",
    "\n",
    "- All cells are frozen except for the ones where you need to submit your solutions or when explicitly mentioned you can interact with it.\n",
    "\n",
    "- You can add new cells to experiment but these will be omitted by the grader, so don't rely on newly created cells to host your solution code, use the provided places for this.\n",
    "\n",
    "- You can add the comment # grade-up-to-here in any graded cell to signal the grader that it must only evaluate up to that point. This is helpful if you want to check if you are on the right track even if you are not done with the whole assignment. Be sure to remember to delete the comment afterwards!\n",
    "\n",
    "- Avoid using global variables unless you absolutely have to. The grader tests your code in an isolated environment without running all cells from the top. As a result, global variables may be unavailable when scoring your submission. Global variables that are meant to be used will be defined in UPPERCASE.\n",
    "\n",
    "- To submit your notebook, save it and then click on the blue submit button at the beginning of the page."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "deletable": false,
    "editable": false,
    "id": "ZpztRwBouwYp",
    "tags": [
     "graded"
    ]
   },
   "outputs": [],
   "source": [
    "import os\n",
    "import base64\n",
    "import numpy as np\n",
    "import tensorflow as tf"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "deletable": false,
    "editable": false
   },
   "outputs": [],
   "source": [
    "import unittests"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Load and inspect the data\n",
    "\n",
    "Begin by loading the data. A couple of things to notice:\n",
    "\n",
    "- The file `mnist.npz` is already included in the current workspace under the `data` directory. By default the `load_data` from Keras accepts a path relative to `~/.keras/datasets` but in this case it is stored somewhere else, as a result of this, you need to specify the full path.\n",
    "\n",
    "- `tf.keras.datasets.mnist.load_data` returns the train and test sets in the form of the tuples `(training_images, training_labels), (testing_images, testing_labels)` but in this exercise you will be needing only the train set so you can ignore the second tuple."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "deletable": false,
    "editable": false,
    "tags": [
     "graded"
    ]
   },
   "outputs": [],
   "source": [
    "# Get current working directory\n",
    "current_dir = os.getcwd()\n",
    "\n",
    "# Append data/mnist.npz to the previous path to get the full path\n",
    "data_path = os.path.join(current_dir, \"data/mnist.npz\")\n",
    "\n",
    "# Load data (discard test set)\n",
    "(training_images, training_labels), _ = tf.keras.datasets.mnist.load_data(path=data_path)\n",
    "\n",
    "print(f\"training_images is of type {type(training_images)}.\\ntraining_labels is of type {type(training_labels)}\\n\")\n",
    "\n",
    "# Inspect shape of the data\n",
    "data_shape = training_images.shape\n",
    "\n",
    "print(f\"There are {data_shape[0]} examples with shape ({data_shape[1]}, {data_shape[2]})\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Pre-processing the data\n",
    "\n",
    "One important step when dealing with image data is to preprocess the data. During the preprocess step you can apply transformations to the dataset that will be fed into your convolutional neural network. This will be your first task of this assignment."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "\n",
    "## Exercise 1: reshape_and_normalize\n",
    "\n",
    "You will apply two transformations to the data:\n",
    "\n",
    "- Reshape the data so that it has an extra dimension at the end, counting the dimensions from left to right (such as you would count in a Python list). The reason for this \n",
    "is that commonly you will use 3-dimensional arrays (without counting the batch dimension) to represent image data. The third dimension represents the color using RGB (Red, Green and Blue) values. This data might be in black and white format so the third dimension doesn't really add any additional information for the classification process but it is a good practice regardless.\n",
    "\n",
    "- Normalize the pixel values so that these are values between 0 and 1. You can achieve this by dividing every value in the array by the maximum pixel value.\n",
    "\n",
    "Remember that these tensors are of type `numpy.ndarray` so you can use functions like [reshape](https://numpy.org/doc/stable/reference/generated/numpy.reshape.html) or [divide](https://numpy.org/doc/stable/reference/generated/numpy.divide.html) to complete the `reshape_and_normalize` function below. Vectorized operations also work!"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "deletable": false,
    "tags": [
     "graded"
    ]
   },
   "outputs": [],
   "source": [
    "# GRADED FUNCTION: reshape_and_normalize\n",
    "\n",
    "def reshape_and_normalize(images):\n",
    "    \"\"\"Reshapes the array of images and normalizes pixel values.\n",
    "\n",
    "    Args:\n",
    "        images (numpy.ndarray): The images encoded as numpy arrays\n",
    "\n",
    "    Returns:\n",
    "        numpy.ndarray: The reshaped and normalized images.\n",
    "    \"\"\"\n",
    "    \n",
    "    ### START CODE HERE ###\n",
    "\n",
    "    # Reshape the images to add an extra dimension (at the right-most side of the array)\n",
    "    images = None\n",
    "    \n",
    "    # Normalize pixel values\n",
    "    images = None\n",
    "    \n",
    "    ### END CODE HERE ###\n",
    "\n",
    "    return images"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Test your function with the next cell:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "deletable": false,
    "editable": false,
    "tags": [
     "graded"
    ]
   },
   "outputs": [],
   "source": [
    "# Reload the images in case you run this cell multiple times\n",
    "(training_images, _), _ = tf.keras.datasets.mnist.load_data(path=data_path)\n",
    "\n",
    "# Apply your function\n",
    "training_images = reshape_and_normalize(training_images)\n",
    "\n",
    "print(f\"Maximum pixel value after normalization: {np.max(training_images)}\\n\")\n",
    "print(f\"Shape of training set after reshaping: {training_images.shape}\\n\")\n",
    "print(f\"Shape of one image after reshaping: {training_images[0].shape}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Expected Output:**\n",
    "```\n",
    "Maximum pixel value after normalization: 1.0\n",
    "\n",
    "Shape of training set after reshaping: (60000, 28, 28, 1)\n",
    "\n",
    "Shape of one image after reshaping: (28, 28, 1)\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "deletable": false,
    "editable": false
   },
   "outputs": [],
   "source": [
    "# Test your code!\n",
    "unittests.test_reshape_and_normalize(reshape_and_normalize)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Exercise 2: EarlyStoppingCallback\n",
    "\n",
    "Now it is time to create your own custom callback. For this complete the `EarlyStoppingCallback` class and the `on_epoch_end` method in the cell below. If you need some guidance on how to proceed, check out this [link](https://www.tensorflow.org/guide/keras/writing_your_own_callbacks)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "deletable": false,
    "tags": [
     "graded"
    ]
   },
   "outputs": [],
   "source": [
    "# GRADED CLASS: EarlyStoppingCallback\n",
    "\n",
    "### START CODE HERE ###\n",
    "\n",
    "# Remember to inherit from the correct class\n",
    "class EarlyStoppingCallback():\n",
    "\n",
    "    # Define the correct function signature for on_epoch_end method\n",
    "    def None():\n",
    "        \n",
    "        # Check if the accuracy is greater or equal to 0.995\n",
    "        if None >= None:\n",
    "                            \n",
    "            # Stop training once the above condition is met\n",
    "            None = None\n",
    "\n",
    "            print(\"\\nReached 99.5% accuracy so cancelling training!\") \n",
    "\n",
    "### END CODE HERE ###"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "deletable": false,
    "editable": false
   },
   "outputs": [],
   "source": [
    "# Test your code!\n",
    "unittests.test_EarlyStoppingCallback(EarlyStoppingCallback)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Exercise 3: convolutional_model\n",
    "\n",
    "Now that you have defined your callback it is time to complete the `convolutional_model` function below. This function should return your convolutional neural network.\n",
    "\n",
    "**Your model should achieve an accuracy of 99.5% or more before 10 epochs to pass this assignment.**\n",
    "\n",
    "**Hints:**\n",
    "- The first layer should take into consideration the `input_shape` of the data, which in this case is the size of each image plus the extra dimension you added earlier.\n",
    "\n",
    "\n",
    "- The last layer should take into account the number of classes you are trying to predict.\n",
    "- Remember you should add a [Conv2d](https://www.tensorflow.org/api_docs/python/tf/keras/layers/Conv2D) layer and a [MaxPooling2D](https://www.tensorflow.org/api_docs/python/tf/keras/layers/MaxPooling2D) layer.\n",
    "- You can try any architecture for the network but try to keep in mind you don't need a complex one. For instance, only one convolutional layer is needed.\n",
    "- In case you need extra help you can check out an architecture that works pretty well at the end of this notebook.\n",
    "- To avoid timeout issues with the autograder, please limit the number of units in your convolutional and dense layers. An exception will be raised if your model is too large."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "deletable": false,
    "tags": [
     "graded"
    ]
   },
   "outputs": [],
   "source": [
    "# GRADED FUNCTION: convolutional_model\n",
    "\n",
    "def convolutional_model():\n",
    "    \"\"\"Returns the compiled (but untrained) convolutional model.\n",
    "\n",
    "    Returns:\n",
    "        tf.keras.Model: The model which should implement convolutions.\n",
    "    \"\"\"\n",
    "\n",
    "    ## START CODE HERE ###\n",
    "\n",
    "    # Define the model\n",
    "    model = tf.keras.models.Sequential([ \n",
    "\t\tNone\n",
    "    ]) \n",
    "\n",
    "    ### END CODE HERE ###\n",
    "\n",
    "    # Compile the model\n",
    "    model.compile(\n",
    "\t\toptimizer='adam',\n",
    "\t\tloss='sparse_categorical_crossentropy',\n",
    "\t\tmetrics=['accuracy']\n",
    "\t)\n",
    "          \n",
    "    return model"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The next cell allows you to check the number of total and trainable parameters of your model and prompts a warning in case these exceeds those of a reference solution, this serves the following 3 purposes listed in order of priority:\n",
    "\n",
    "- Helps you prevent crashing the kernel during training.\n",
    "\n",
    "- Helps you avoid longer-than-necessary training times.\n",
    "- Provides a reasonable estimate of the size of your model. In general you will usually prefer smaller models given that they accomplish their goal successfully.\n",
    "\n",
    "**Notice that this is just informative** and may be very well below the actual limit for size of the model necessary to crash the kernel. So even if you exceed this reference you are probably fine. However, **if the kernel crashes during training or it is taking a very long time and your model is larger than the reference, come back here and try to get the number of parameters closer to the reference.**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "deletable": false,
    "editable": false
   },
   "outputs": [],
   "source": [
    "# Define your compiled (but untrained) model\n",
    "model = convolutional_model()\n",
    "\n",
    "# Check parameter count against a reference solution\n",
    "unittests.parameter_count(model)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "deletable": false,
    "editable": false
   },
   "outputs": [],
   "source": [
    "# Train your model (this can take up to 5 minutes)\n",
    "training_history = model.fit(training_images, training_labels, epochs=10, callbacks=[EarlyStoppingCallback()])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Expected Output:**\n",
    "\n",
    "`Reached 99.5% accuracy so cancelling training!` printed out before reaching 10 epochs.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "deletable": false,
    "editable": false
   },
   "outputs": [],
   "source": [
    "# Test your code!\n",
    "unittests.test_training_history(training_history)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Need more help?\n",
    "\n",
    "Run the following cell to see an architecture that works well for the problem at hand:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "deletable": false,
    "editable": false
   },
   "outputs": [],
   "source": [
    "# WE STRONGLY RECOMMEND YOU TO TRY YOUR OWN ARCHITECTURES FIRST\n",
    "# AND ONLY RUN THIS CELL IF YOU WISH TO SEE AN ANSWER\n",
    "\n",
    "encoded_answer = \"CiAgIC0gQSB0Zi5rZXJhcy5JbnB1dCB3aXRoIGEgc2hhcGUgdGhhdCBtYXRjaGVzIHRoYXQgb2YgZXZlcnkgaW1hZ2UgaW4gdGhlIHRyYWluaW5nIHNldCBwbHVzIHRoZSBleHRyYSBkaW1lbnNpb24geW91IGFkZGVkCiAgIC0gQSBDb252MkQgbGF5ZXIgd2l0aCAzMiBmaWx0ZXJzLCBhIGtlcm5lbF9zaXplIG9mIDN4MywgUmVMVSBhY3RpdmF0aW9uIGZ1bmN0aW9uCiAgIC0gQSBNYXhQb29saW5nMkQgbGF5ZXIgd2l0aCBhIHBvb2xfc2l6ZSBvZiAyeDIKICAgLSBBIEZsYXR0ZW4gbGF5ZXIgd2l0aCBubyBhcmd1bWVudHMKICAgLSBBIERlbnNlIGxheWVyIHdpdGggMTI4IHVuaXRzIGFuZCBSZUxVIGFjdGl2YXRpb24gZnVuY3Rpb24KICAgLSBBIERlbnNlIGxheWVyIHdpdGggMTAgdW5pdHMgYW5kIHNvZnRtYXggYWN0aXZhdGlvbiBmdW5jdGlvbgo=\"\n",
    "encoded_answer = encoded_answer.encode('ascii')\n",
    "answer = base64.b64decode(encoded_answer)\n",
    "answer = answer.decode('ascii')\n",
    "\n",
    "print(answer)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Congratulations on finishing this week's assignment!**\n",
    "\n",
    "You have successfully implemented a CNN to assist you in the image classification task. Nice job!\n",
    "\n",
    "**Keep it up!**"
   ]
  }
 ],
 "metadata": {
  "grader_version": "1",
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.5"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 1
}
